package com.hrvsr.downloader.httpclient;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import java.util.List;

import org.apache.commons.io.IOUtils;
import org.apache.http.HttpHost;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.protocol.HttpContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.hrvsr.common.NameValuePair;
import com.hrvsr.common.Request;
import com.hrvsr.common.Response;
import com.hrvsr.common.consts.Consts;
import com.hrvsr.common.util.CollectionUtil;
import com.hrvsr.common.util.StrUtil;
import com.hrvsr.downloader.ClosableSafeDownloader;
import com.hrvsr.downloader.Downloader;
import com.hrvsr.downloader.httpclient.session.CookieStoreApater;
import com.hrvsr.session.Session;
import com.hrvsr.session.cookie.Cookies;
import com.hrvsr.session.header.Header;
import com.hrvsr.session.proxy.Proxy;
import com.hrvsr.session.useragent.AbstractNotNullUserAgentPool;

@SuppressWarnings("unused")
public class HttpClientDownloaderBuilder {
	private volatile RequestConversionStrategy requestConversionStrategy;
	private volatile ResponseConversionStrategy responseConversionStrategy;
	private CloseableHttpClient httpClient;

	// ----------configs---------------
	private int connPoolSize = 4;
	private int connectTimeOut = 10000; // milliseconds
	private int retryTimes = 3;

	// custom
	public static HttpClientDownloaderBuilder custom() {
		return new HttpClientDownloaderBuilder();
	}

	// ------------------build
	public Downloader build() {
		InternalHttpClientDownloader downloader = new InternalHttpClientDownloader();
		if (httpClient == null) {
			downloader.httpClient = ClientFactory.getNewClient(connectTimeOut, retryTimes, connPoolSize);
		} else {
			// downloader.httpClient =
			// ClientFactory.getNewClient(connectTimeOut, retryTimes);
		}

		if (requestConversionStrategy == null) {
			requestConversionStrategy = new DefaultRequestConversionStrategy();
		}

		if (responseConversionStrategy == null) {
			responseConversionStrategy = new DefaultResponseConvertStrategy();
		}

		downloader.requestConversionStrategy = requestConversionStrategy;
		downloader.responseConvertStrategy = responseConversionStrategy;

		return ClosableSafeDownloader.adapt(downloader);
//		return downloader;
	};

	// ------------setters and getters--------------
	public RequestConversionStrategy getRequestConversionStrategy() {
		return requestConversionStrategy;
	}

	public HttpClientDownloaderBuilder setRequestConversionStrategy(
			RequestConversionStrategy requestConversionStrategy) {
		this.requestConversionStrategy = requestConversionStrategy;
		return this;
	}

	public ResponseConversionStrategy getResponseConversionStrategy() {
		return responseConversionStrategy;
	}

	public HttpClientDownloaderBuilder setResponseConversionStrategy(
			ResponseConversionStrategy responseConversionStrategy) {
		this.responseConversionStrategy = responseConversionStrategy;
		return this;
	}

	public CloseableHttpClient getHttpClient() {
		return httpClient;
	}

	public HttpClientDownloaderBuilder setHttpClient(CloseableHttpClient httpClient) {
		this.httpClient = httpClient;
		return this;
	}

	public int getThreadNum() {
		return connPoolSize;
	}

	public HttpClientDownloaderBuilder setConnPoolSize(int threadNum) {
		this.connPoolSize = threadNum;
		return this;
	}

	public int getConnectTimeOut() {
		return connectTimeOut;
	}

	public HttpClientDownloaderBuilder setConnectTimeOut(int connectTimeOut) {
		this.connectTimeOut = connectTimeOut;
		return this;
	}

	public int getRetryTimes() {
		return retryTimes;
	}

	public HttpClientDownloaderBuilder setRetryTimes(int retryTimes) {
		this.retryTimes = retryTimes;
		return this;
	}

	// -------interval classes
	private static class DefaultRequestConversionStrategy implements RequestConversionStrategy {

		@Override
		public HttpUriRequest convert(Request request, Session session) {
			RequestBuilder builder = RequestBuilder.create(request.getMehtod());
			builder.setUri(request.getUrl());
			builder.addHeader("Accept-Encoding", "gzip");
			Iterator<NameValuePair> params = request.paramIterator();
			if (params != null) {
				while (params.hasNext()) {
					NameValuePair kv = params.next();
					builder.addParameter(kv.getName(), kv.getValue());
				}
			}

			if (session != null) {
				// set user agent
				String userAgent = session.getUserAgent();
				builder.addHeader("User-Agent",
						StrUtil.isEmpty(userAgent) ? AbstractNotNullUserAgentPool.DEFAUTL_USER_AGENT : userAgent);

				// set proxy
				Proxy proxy = session.getProxy();
				if (proxy != null) {
					RequestConfig requestConfig = RequestConfig.custom()
							.setProxy(new HttpHost(proxy.getHost(), proxy.getPort())).build();
					builder.setConfig(requestConfig);
				}

				// add headers
				List<Header> headers = session.getHeaders();
				if (CollectionUtil.isNotEmpty(headers)) {
					for (Header h : headers) {
						builder.addHeader(h.getName(), h.getValue());
					}
				}
			}

			return builder.build();
		}

	}

	private static class DefaultResponseConvertStrategy implements ResponseConversionStrategy {
		private static Logger logger = LoggerFactory.getLogger(DefaultResponseConvertStrategy.class);

		@Override
		public Response convert(Request request, CloseableHttpResponse resp) {
			HttpClientResponseAdapter ans = new HttpClientResponseAdapter();
			ans.request = request;
			ans.closeableHttpResp = resp;
			InputStream in = null;
			try {
				in = resp.getEntity().getContent();
				byte[] bytes = IOUtils.toByteArray(in);
				ans.content = bytes;
			} catch (UnsupportedOperationException | IOException e) {
				logger.error("error convert httpclient response to Hrvsr respone", e);
				ans.getRequest().putExtraInfo(Consts.DOWN_LOAD_ERR, e.toString());
			} finally {
				IOUtils.closeQuietly(in);
			}
			return ans;
		}
	}

	private static class InternalHttpClientDownloader implements Downloader, Closeable {
		private static Logger logger = LoggerFactory.getLogger(InternalHttpClientDownloader.class);
		private CloseableHttpClient httpClient;
		private RequestConversionStrategy requestConversionStrategy;
		private ResponseConversionStrategy responseConvertStrategy;

		@Override
		public Response download(Request request, Session session) {
			HttpUriRequest httpUriRequest = genRequest(request, session);
			Response resp = getInitialResp(request);
			HttpContext ctx = genContext(session);
			try {
				CloseableHttpResponse httpResp = httpClient.execute(httpUriRequest, ctx);
				resp = genResponse(request, httpResp);
			} catch (Exception e) {
				logger.error("error downloading {}", request.toString(), e);
				request.putExtraInfo(Consts.DOWN_LOAD_ERR, e.toString());
				if (session != null) {
					request.putExtraInfo(Consts.SESSION_SNAPSHOT, session.toString());
				}
			} finally {
				visited(request, session);
				((HttpRequestBase) httpUriRequest).releaseConnection();
			}
			return resp;
		}

		private void visited(Request request, Session session) {
			Object o = request.getExtraInfo(Consts.VISITED_TIMES);
			if (o == null) {
				request.putExtraInfo(Consts.VISITED_TIMES, 1);
			} else {
				int n = (int) o;
				request.putExtraInfo(Consts.VISITED_TIMES, ++n);
			}
			
			if (session != null)
				request.putExtraInfo(Consts.SESSION_SNAPSHOT, session.toString());
		}

		private Response getInitialResp(Request request) {
			HttpClientResponseAdapter resp = new HttpClientResponseAdapter();
			resp.request = request;
			return resp;
		}

		private HttpUriRequest genRequest(Request request, Session session) {
			return requestConversionStrategy.convert(request, session);
		}

		private Response genResponse(Request request, CloseableHttpResponse resp) {
			return responseConvertStrategy.convert(request, resp);
		}

		private HttpContext genContext(Session session) {
			HttpClientContext ctx = new HttpClientContext();
			if (session != null) {
				Cookies cookies = session.getCookies();
				if (cookies != null)
					ctx.setCookieStore(((CookieStoreApater) cookies).getCookieStore());
			}
			return ctx;
		}

		@Override
		public void close() throws IOException {
			httpClient.close();
		}
	}

	private static class HttpClientResponseAdapter implements Response {
		private Request request;
		private CloseableHttpResponse closeableHttpResp;
		private byte[] content;

		public HttpClientResponseAdapter() {
		}

		public HttpClientResponseAdapter(CloseableHttpResponse closeableHttpResp) {
			super();
			this.closeableHttpResp = closeableHttpResp;
		}

		@Override

		public Request getRequest() {
			return request;
		}

		@Override
		public int getStatusCode() {
			if (closeableHttpResp != null) {
				return closeableHttpResp.getStatusLine().getStatusCode();
			}
			return -1;
		}

		@Override
		public String getContentType() {
			return null;
		}

		@Override
		public byte[] getContent() {
			return content;
		}

	}

}
